The best tricks and tricks for native JavaScript syntax
The times before ECMAScript 6 or ECMAScript 2015 must have been very dark. At least, this is my impression when I look at the skyrocketing development that JavaScript’s image has taken since then. While the programming language was once mainly concerned with its role as a joke, it is now accepted among its peers. In fact, not much has really changed. JavaScript is still a primarily imperative, dynamically typed, and rather object-oriented language. Despite many new features since the “joke era”, not much has changed in the way we write JS. Very few features have really turned the standard approach to typical JS challenges upside down.
For example, the JS mainstream, since the advent of async/await, solves asynchronous tasks in a completely new way (imperative instead of functional with promises, or chaotic with callbacks). Even the introduction of classes hasn’t really had a comparable effect, because if you wanted “real” OOP, you could have had it during ES3 times, but with a hand-made implementation of class concepts.
The little things are also decisive
What does it tell us when we celebrate little things like a uniform class syntax or syntax sweeteners like destructuring and arrow functions, as well as the introduction of the revolutionary async/await? It tells us that the so-called little things are important. It makes a difference that there is a standard solution for class OOP, because it reduces mental overhead – there is simply one less problem to solve. And it makes a difference that we can use destructuring to break up objects, because it makes code more compact – there are fewer lines of code that take up mental memory. Little things can make a difference.
These little things do not always have to be official features. Anyone who occasionally (re)builds things in the physical world knows that great tools and exquisite materials are the foundation of a successful project, but nothing more. You also have to understand how to build on this foundation. Besides the Big Picture, it’s often the little things, the seemingly obvious actions, that make a huge difference to the fun-factor. Of course, you always reach your goal somehow, but when the master, the mum, or the guru show you simple tricks, you can solve a problem in a very simple and elegant way. Then solving it is twice as fun.
In this article, I will introduce you to some JavaScript handles that are in the same league as a professional mango opening or Uncle Heinz’ simple trick for the fumble-free installation of flush-mounted satellite sockets. Perhaps some readers already know some or even all of these tricks, or perhaps they seem obvious. But they make our daily JavaScript life a lot more pleasant and our code more elegant.
1. Remove duplicates from lists
To de-duplicate lists in the Cretaceous period, it was still necessary to use library functions like _.uniq() from lo-dash. Nowadays this operation can be a native one-liner (Listing 1).
Listing 1: Simple deduplication with spreads and arrays
const input = [ 0, 1, 2, 1, 0, 2, 3, 1 ]; const unique = [ ...new Set(input) ]; // what remains in "unique" [ 0, 1, 2, 3 ]
The combination of sets with array spread ensures that all doubles are filtered out of the input array. The whole operation consists of two substeps:
- new Set(input) constructs a set from the input array, which by definition can only contain each value once. Duplicates are automatically ejected at this point without changing the order.
- The array spread converts the contents of the set back into an array. Since our input object was an array, we probably want to have an array as the output again and not a set – the set is purely for deduplication purposes.
It is important that the set input does not have to be an array, but can be any object that implements the iterative protocol. In short, anything that can be processed by a for-of-loop can be made into a set and freed from duplicates. In addition to arrays, this includes NodeLists, DomStringLists, and the iterators that provide the methods values() and keys() for maps.
The only catch here is that the equality of values of sets is determined by the SameValueZero algorithm. This works almost like a comparison with ===, except that NaN is considered equal to NaN, which is normally not the case. However, this also means that for references, two different but content-equivalent objects are not recognized as duplicates, which is preferable in many circumstances. In such cases, there is no way around a detailed comparison of the objects or a comparison of object hashes.
2. Delete data from arrays
Even if the web development zeitgeist currently favors immutability, it is sometimes quite practical to change the content of data structures. With the newer standard objects Map and Set it is very easy to delete all data: call the clear() method and that’s it! Such a method does not exist directly for the older arrays, but there is a rather simple alternative (Listing 2).
Listing 2: Empty an array
myArray.length = 0;
The length property of arrays always reflects the number of fields contained in the array, but the ability for this property to be set is not often known to many JavaScript developers. This is not only the best way to empty an array completely, but also the best way to trim it (Listing 3).
Listing 3: Trimming an array
let myArray = [ "a", "b", "c" ]; myArray.length = 2; // what is left: [ "a", "b" ]
Conversely, targeted manipulation of the length property can also create additional fields in an array (Listing 4).
Listing 4: Extending an Array
let myArray = [ "a", "b", "c" ]; myArray.length = 5; for (let value of myArray) { console.log(value); } // > "a" // > "b" // > "c" // > undefined // > undefined
By the way, the length field of arrays is a nice example for the fact that there have always been integers in JavaScript. While the type of length has always been “number”, it was always impossible to set a length that was not an integer, or positive number (Listing 5).
Listing 5: Integers as array length
let x = []; x.length = 23.42 > RangeError: Invalid array length x.length = -42 > RangeError: Invalid array length
On the other hand, the value for length must not be a real integer, because a BigInt may not have the length property either (Listing 6).
Listing 6: Real integers as array length
let x = []; x.length = 42n; // n-Suffix = BigInt > Uncaught TypeError: Cannot convert a BigInt value to a number
So the length property of a JavaScript array is many things at once: It is a number (but not quite), a bit of an integer (but not really), and most of all a wonderful replacement for a clear() method.
3. Named callbacks for setTimeout and Co
When a function needs to be executed at regular intervals, the inclined JS developer usually uses the setInterval() method. However, this is only the right choice as long as the function to be executed remains fast and synchronous. If the function to be executed becomes cumbersome or handles asynchronous callbacks, it is better to recursively call setTimeout(), like in Listing 7.
Listing 7: Recursive setTimeout, but how?
setTimeout( function () { doStuff(); setTimeout(???, 1000); // was tun? }, 1000);
But what happens where the question marks are in the code snippet? The answer is as simple as it is irritating: The name of the function, which we can actually easily assign to the callback function, as you can see in Listing 8.
Listing 8: Recursive setTimeout!
setTimeout( function myFunc () { doStuff(); setTimeout(myFunc, 1000); }, 1000);
This is one of the extremely rare places in JavaScript where named function expressions are good for something. Traditional JS usually uses only two types of function definitions, namely normal function declarations and anonymous function expressions (Listing 9).
Listing 9: Common JS functions
// Function declaration function foo () { return 42; } // anonymous function expressions let foo = function () { return 42; };
The crucial difference here is not whether the function keyword is followed by the name foo, but that the second function is located on the right side of an assignment, and is therefore basically an input for something else. For function declarations that stand on their own (first function definition), a name is mandatory, otherwise there would be no way to reference the function. This does not mean, however, that function expressions should not have a name! Normally they don’t need one, because they are stuffed either in variables or as callbacks into other functions, but in principle, you can have a name (Listing 10).
Listing 10: Anonymous function expression with name
let foo = function bar () { return 42; };
The effect of such a construction is a function that:
- is found outside itself in the variable foo.
- within itself can be found both under foo and under bar.
- has a name property containing bar.
Such functions are actually only useful if they are used as callback functions, which should remain referenceable beyond their role as parameters, as is the case with recursive use of setTimeout(). Another example would be the Retry feature in Listing 11.
Listing 11: Fetch with Retry
fetch("/some/data").then( function handleResponse (response) { if (!response.ok) { fetch("/some/data").then(handleResponse); // try again } else { somethingElse(response); } });
Named Function Expressions are one of the special features that can only be implemented with functions defined by the function keyword. Arrow Functions are also function expressions, but their syntax does not allow the assignment of a name. Instead, its name property takes the name of the variable to which they are assigned, provided such a variable exists and the function is not simply a callback.
4. Transform and filter in one step
If you like functional style programming, you can transform arrays with the help of the map() method and filter them with the filter() method. But what do you do if transforming and filtering should happen all at once? For example, what if a large array should not be run through twice? This is no problem with flatMap() as shown in Listing 12.
Listing 12: flatMap in Action
let numbers = [ 0, 1, 2, 3, 4, 5, 6 ]; let doubledEvens = numbers.flatMap( (n) => n % 2 === 0 ? [ n * 2 ] : [] ); // result: [ 0, 4, 8, 12 ]
The actual purpose of flatMap() is to inflate an array during a transformation. While the map method transforms each value in the input array to exactly one other value in the output array, flatMap() can generate any number of values in the output array from each value in the input array – one per element in the array returned by the transformation function. And this arbitrary set of values can be 0!
Combining two operations in one step can be useful in different circumstances. Apart from the fact that it is helpful for simple economical code reasons to combine trivial calculations (as in the example above), performance can also play a role. It is easy to first filter an array and then transform it (Listing 13), whereas it is already more problematic when it first has to be transformed and then filtered out! If it is possible to determine before an operation whether it has to be performed, then this should be done. Otherwise, unnecessary computations will result, and we cannot expect the magic of the JS engines to optimize our performance.
Listing 13: First filtered, then transformed
let numbers = [ 0, 1, 2, 3, 4, 5, 6 ]; let doubledEvens = numbers .filter( (n) => n % 2 === 0 ) .map( (n) => n * 2 ); // same result [ 0, 4, 8, 12 ]
5. Object destructuring for arrays
At first glance, JavaScript arrays are nothing more than objects with numeric keys. The differences between an array and a fake array are marginal (Listing 14).
Listing 14: Fake Array
let fakeArray = { "0": "Hello", "1": "World", "length": 2 }; let x = fakeArray[1]; // x === "World"
Access via index works (the index specified as the number is automatically stringified and then matches the corresponding object property), with a vanilla-for loop. This way the fake array can be cued up without problems and with moderate use, an inclined developer could also add methods like push() and map() to the fake array or implement iteration protocols.
The iteration protocol would be necessary to enable array destructuring for the fake array. The convenient extraction syntax with the square brackets, as shown in Listing 15 is not an array specific feature at all, but a feature of the iterative protocol. Therefore, the so-called “array destructuring” can be used with everything that is compatible with for-of-loops and not only with arrays. For normal objects, however, there are no special requirements, which is why normal object destructuring always works with any kind of object (Listing 16).
Listing 15: Array Destructuring
let array = [ "A", "B", "C", "D", "E", "F", "G", "H", "I" ]; let [ first, second ] = array; // first === "A", second === "B"
Listing 16: Object Destructuring
let obj = { foo: 23, bar: 42, baz: 1337 }; let { foo, baz } = obj; // foo === 23, baz === 1337
The normal object destructuring has the advantage that we can simply ignore unwanted fields – if we don’t need bar, we just don’t list it. With array destructuring, omitting fields is a bit less attractive, as Listing 17 shows.
Listing 17: Array Destructuring with Omissions
let array = [ "A", "B", "C", "D", "E", "F", "G", "H", "I" ]; let [ first, , , fourth ] = array; // first === "A", fourth === "D"
Since array destructuring uses the iterative protocol, and the second and third elements unfortunately come before the fourth element, we have to omit the second and third elements with additional commas. At the latest, when we want to get to the first, seventh and ninth element, the comma salad becomes confusing – unless we simply misuse object destructuring for arrays (Listing 18)!
Listing 18: Object Destructuring for Arrays
let array = [ "A", "B", "C", "D", "E", "F", "G", "H", "I" ]; let { 0: foo, 6: bar, 8: baz } = array; // foo === "A", bar === "G", baz === "I"
As mentioned in the beginning, arrays are little more than objects with numeric keys and we also know that objects can be traced with object destructuring without any special precautions. It follows that we can apply object destructuring to arrays and use the indexes as field names! Although purely numeric variable names are not allowed, this is not a problem with the renaming syntax of Object Restructuring. The syntax { a: x } = o means “extract from the value of field a and write it to variable x”, which is wonderfully applicable to the numeric index keys of our Array Object Restructuring. This trick is especially useful when we apply it to arrays from which we want to extract more than just a few fields. For example, with Array Object Restructuring we can extract not only values but also the length property in a single, convenient declaration (Listing 19).
Listing 19: Extract fields and length
let array = [ "A", "B", "C", "D", "E", "F", "G", "H", "I" ]; let { 6: bar, 8: baz, length } = array; // bar === "G", baz === "I", length === 9
The real upper hand is the combination of array object destructuring with RegExpArrays as spat out by the exec() method of regular expressions. In addition to values at numeric indices, these contain the input string as well as the index of the current location in the input. All information that can be extracted from the very confusingly constructed RegExpArray by means of Array-Object-Destructuring (Listing 20)!
Listing 20: Processing RegExpArray
let { 0: match, index, input } = /bar/.exec("foobar"); // match === "bar", index === 3, input === "foobar"
If it’s only about normal values in reasonably used arrays, then array object destructuring is often not necessary. But for those cases where, in addition to normal fields, the length (or in the case of RegExpArrays, much more) needs to be extracted, this JavaScript handle is one of the more practical ones.